#pragma once
#include <thread>
#include <condition_variable>
#include <functional>
#include <chrono>

template <typename T>
struct Message
{
    typedef std::function<void()> VoidFunction;
public:
    Message *next;
    VoidFunction f;
    std::time_t when;
    T token;
    Message(VoidFunction fn, std::time_t w, T t) : next(nullptr), f(fn), when(w), token(t)
    {
    }
};

template <typename T, typename Compare = std::equal_to<T>>
class ThreadLooper
{
private:
    Compare compare_;
    std::condition_variable cond_;
    std::mutex mutex_;
    Message<T> *messages_;
    bool quitting_;
    std::thread *thread_;
    Message<T> *next()
    {
        for (;;)
        {
            std::unique_lock<std::mutex> lock(mutex_);

            auto now = getCurrentTime();
            Message<T> *msg = messages_;

            if (msg != nullptr)
            {
                if (now < msg->when)
                {
                    int64_t t = msg->when - now;
                    if (t < 0)
                    {
                        t = 0;
                    }
                    cond_.wait_for(lock, std::chrono::milliseconds(t), [&]()
                                   { return quitting_; });
                }
                else
                {
                    messages_ = msg->next;
                    msg->next = nullptr;
                    return msg;
                }
            }
            else
            {
                cond_.wait(lock, [&]()
                           { return messages_ != nullptr || quitting_; });
            }

            if (quitting_)
            {
                return nullptr;
            }
        }
    }

    void run()
    {
        for (;;)
        {
            Message<T> *msg = next();
            if (msg == nullptr)
            {
                return;
            }
            msg->f();
            delete msg;
        }
    }

    bool enqueueMessage(Message<T> *msg)
    {
        std::unique_lock<std::mutex>(mutex);
        if (quitting_)
        {
            return false;
        }

        Message<T> *p = messages_;
        if (p == nullptr || msg->when == 0 || msg->when < p->when)
        {
            msg->next = p;
            messages_ = msg;
        }
        else
        {
            Message<T> *prev;
            for (;;)
            {
                prev = p;
                p = p->next;
                if (p == nullptr || msg->when < p->when)
                {
                    break;
                }
            }
            msg->next = p;
            prev->next = msg;
        }
        cond_.notify_one();

        return true;
    }

    inline int64_t getCurrentTime()
    {
        using namespace std::chrono;
        return duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
    }

public:
    ThreadLooper() : compare_(), messages_(nullptr), quitting_(false)
    {
        thread_ = new std::thread(std::bind(&ThreadLooper::run, this));
    }

    ~ThreadLooper()
    {
        if (!quitting_)
        {
            quit();
        }
    }

    void quit()
    {
        {
            std::unique_lock<std::mutex>(mutex);
            if (quitting_)
            {
                return;
            }

            quitting_ = true;

            Message<T> *p = messages_;
            while (p != nullptr)
            {
                Message<T> *n = p->next;
                delete p;
                p = n;
            }
            messages_ = nullptr;
            cond_.notify_one();
        }
        if (thread_ != nullptr)
        {
            thread_->join();
            delete thread_;
            thread_ = nullptr;
        }
    }

    size_t remove(const T &token)
    {
        std::unique_lock<std::mutex> lock(mutex_);
        Message<T> *p = messages_;
        size_t count = 0;
        while (p != nullptr && compare_(p->token, token))
        {
            Message<T> *n = p->next;
            messages_ = n;
            delete p;
            ++count;
            p = n;
        }
        while (p != nullptr)
        {
            Message<T> *n = p->next;
            if (n != nullptr)
            {
                if (n->token == token)
                {
                    Message<T> *nn = n->next;
                    delete n;
                    ++count;
                    p->next = nn;
                    continue;
                }
            }
            p = n;
        }
        return count;
    }

    void enqueue(std::function<void()> fn, T token)
    {
        return enqueueDelayed(std::move(fn), 0, token);
    }

    void enqueueDelayed(std::function<void()> fn, time_t delayMillis, T token)
    {
        return enqueueAtTime(std::move(fn), getCurrentTime() + delayMillis, token);
    }
    
    void enqueueAtTime(std::function<void()> fn, time_t uptimeMillis, T token)
    {
        Message<T> *msg = new Message<T>(std::move(fn), uptimeMillis, token);
        if (!enqueueMessage(msg))
        {
            delete msg;
        }
    }
};